# Customise this file, documentation can be found here:
# https://github.com/fastlane/fastlane/tree/master/docs
# All available actions: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Actions.md
# can also be listed using the `fastlane actions` command

# Change the syntax highlighting to Ruby
# All lines starting with a # are ignored when running `fastlane`

# If you want to automatically update fastlane if a new version is available:
# update_fastlane

# This is the minimum version number required.
# Update this, if you use features of a newer version
fastlane_version '2.137.0'
default_platform :ios

before_all do
  setup
end

private_lane :setup do
  ENV['FASTLANE_USER'] = ENV.fetch('HOMEASSISTANT_APPLE_ID', nil)
  ENV['DELIVER_USERNAME'] = ENV.fetch('HOMEASSISTANT_APPLE_ID', nil)
  ENV['FASTLANE_TEAM_ID'] = ENV.fetch('HOMEASSISTANT_TEAM_ID', nil)
  ENV['FL_NOTARIZE_ASC_PROVIDER'] = ENV.fetch('HOMEASSISTANT_TEAM_ID', nil)
  ENV['PILOT_APPLE_ID'] = ENV.fetch('HOMEASSISTANT_APPLE_ID', nil)
  ENV['SIGH_USERNAME'] = ENV.fetch('HOMEASSISTANT_APPLE_ID', nil)
  ENV['FL_NOTARIZE_USERNAME'] = ENV.fetch('HOMEASSISTANT_APPLE_ID', nil)
  ENV['LOKALISE_API_TOKEN'] = ENV.fetch('HOMEASSISTANT_LOKALIZE_TOKEN', nil)
  ENV['LOKALISE_PROJECT_ID'] = ENV.fetch('HOMEASSISTANT_LOKALIZE_PROJECT_ID', nil)
  ENV['LOKALISE_PROJECT_ID_FRONTEND'] = ENV.fetch('HOMEASSISTANT_LOKALIZE_PROJECT_FRONTEND', nil)
  ENV['LOKALISE_PROJECT_ID_CORE'] = ENV.fetch('HOMEASSISTANT_LOKALIZE_PROJECT_CORE', nil)
  ENV['PILOT_ITC_PROVIDER'] = ENV.fetch('HOMEASSISTANT_APP_STORE_CONNECT_TEAM_ID', nil)
  ENV['FASTLANE_ITC_TEAM_ID'] = ENV.fetch('HOMEASSISTANT_APP_STORE_CONNECT_TEAM_ID', nil)
  ENV['SIGH_USERNAME'] = ENV.fetch('HOMEASSISTANT_APPLE_ID', nil)
  ENV['SIGH_PROFILE_PATHS'] = '../Configuration/Provisioning'

  app_store_connect_api_key if ENV['APP_STORE_CONNECT_API_KEY_KEY']
end

lane :lint do
  sh('cd .. ; Pods/SwiftFormat/CommandLineTool/swiftformat --config .swiftformat --lint --quiet .')
  sh('cd .. ; Pods/SwiftLint/swiftlint lint --config .swiftlint.yml --quiet .')
  sh('cd .. ; bundle exec rubocop --config .rubocop.yml')
end

lane :autocorrect do
  sh('../Pods/SwiftFormat/CommandLineTool/swiftformat ..')
  sh('bundle exec rubocop -a ..')
end

lane :download_provisioning_profiles do
  %w[ios catalyst macos].each do |platform|
    sh(
      'bundle', 'exec', 'fastlane', 'sigh', 'repair',
      '-p', platform,
      '-b', ENV.fetch('HOMEASSISTANT_TEAM_ID', nil),
      '-u', ENV.fetch('SIGH_USERNAME', nil)
    )
    sh(
      'bundle', 'exec', 'fastlane', 'sigh', 'download_all',
      '-p', platform,
      '-o', '../Configuration/Provisioning/',
      '-b', ENV.fetch('HOMEASSISTANT_TEAM_ID', nil),
      '-u', ENV.fetch('SIGH_USERNAME', nil)
    )
  end
end

lane :import_provisioning_profiles do
  directory = '../Configuration/Provisioning'

  Dir.children(directory).each do |file|
    next if file.start_with?('.')

    install_provisioning_profile(path: File.expand_path(File.join(directory, file)))
  end
end

lane :update_dsyms do
  directory = File.expand_path('dSYMs')
  FileUtils.mkdir_p directory

  download_dsyms(
    after_uploaded_date: Date.today.prev_day(7).iso8601,
    app_identifier: 'io.robbie.HomeAssistant',
    output_directory: directory
  )

  FileUtils.rm_r directory
end

desc 'Update the test cases from the fcm repo'
lane :update_notification_test_cases do
  bundle_directory = File.expand_path('../Tests/Shared/notification_test_cases.bundle')
  zip_file = Tempfile.new(['archive', '.zip'])

  FileUtils.rm_rf bundle_directory
  FileUtils.mkdir_p bundle_directory

  begin
    archive_url = 'https://github.com/home-assistant/mobile-apps-fcm-push/archive/refs/heads/master.zip'
    unzip_path = 'mobile-apps-fcm-push-master/functions/test/fixtures/legacy/*.json'

    sh("curl -L #{archive_url} -o #{zip_file.path}")
    sh("unzip -j #{zip_file.path} -d #{bundle_directory} '#{unzip_path}'")
  ensure
    zip_file.unlink
  end
end

desc 'Generate proper icons for all build trains'
lane :icons do
  appicon(appicon_path: 'Sources/App/Resources/Assets.xcassets', appicon_image_file: 'icons/dev.png',
          appicon_name: 'AppIcon.dev.appiconset', appicon_devices: %i[ipad iphone ios_marketing macos])
  appicon(appicon_path: 'Sources/App/Resources/Assets.xcassets', appicon_image_file: 'icons/beta.png',
          appicon_name: 'AppIcon.beta.appiconset', appicon_devices: %i[ipad iphone ios_marketing macos])
  appicon(appicon_path: 'Sources/App/Resources/Assets.xcassets', appicon_image_file: 'icons/release.png',
          appicon_devices: %i[ipad iphone ios_marketing macos])

  appicon(appicon_path: 'WatchApp/Assets.xcassets', appicon_image_file: 'icons/dev.png',
          appicon_name: 'WatchIcon.dev.appiconset', appicon_devices: %i[watch watch_marketing])
  appicon(appicon_path: 'WatchApp/Assets.xcassets', appicon_image_file: 'icons/beta.png',
          appicon_name: 'WatchIcon.beta.appiconset', appicon_devices: %i[watch watch_marketing])
  appicon(appicon_path: 'WatchApp/Assets.xcassets', appicon_image_file: 'icons/release.png',
          appicon_name: 'WatchIcon.appiconset', appicon_devices: %i[watch watch_marketing])
end

desc 'Update switftgen input/output files'
lane :update_swiftgen_config do
  # rubocop:disable Layout/LineLength
  sh('cd ../ && ./Pods/SwiftGen/bin/swiftgen config generate-xcfilelists --inputs swiftgen.yml.file-list.in --outputs swiftgen.yml.file-list.out')
  # rubocop:enable Layout/LineLength
end

desc 'Download latest localization files from Lokalize'
lane :update_strings do
  token = ENV.fetch('LOKALISE_API_TOKEN') do
    prompt(
      text: 'API token',
      secure_text: true
    )
  end

  project_id = ENV.fetch('LOKALISE_PROJECT_ID') do
    prompt(
      text: 'Project ID',
      secure_text: true
    )
  end

  frontend_project_id = ENV.fetch('LOKALISE_PROJECT_ID_FRONTEND') do
    prompt(
      text: 'Frontend Project ID',
      secure_text: true
    )
  end

  core_project_id = ENV.fetch('LOKALISE_PROJECT_ID_CORE') do
    prompt(
      text: 'Core Project ID',
      secure_text: true
    )
  end

  resources_dir_full = File.expand_path('../Sources/App/Resources')

  sh(
    'lokalise2',
    '--token', token,
    '--project-id', project_id,
    'file', 'download',
    '--format', 'ios_sdk',
    '--export-empty-as', 'base',
    '--export-sort', 'a_z',
    '--replace-breaks=false',
    '--include-comments=false',
    '--include-description=false',
    '--unzip-to', resources_dir_full,
    log: false
  )

  lang_frontend_to_ios = {
    'bg' => 'bg',
    'ca' => 'ca-ES',
    'cs' => 'cs',
    'cy' => 'cy-GB',
    'da' => 'da',
    'de' => 'de',
    'el' => 'el',
    'en-GB' => 'en-GB',
    'en' => 'en',
    'es' => 'es-ES',
    'es-419' => 'es',
    # es-MX is missing from the frontend, so we copy es over below
    'et' => 'et',
    'fi' => 'fi',
    'fr' => 'fr',
    'he' => 'he',
    'hu' => 'hu',
    'id' => 'id',
    'it' => 'it',
    'ja' => 'ja',
    'ko' => 'ko-KR',
    'ml' => 'ml',
    'nb' => 'nb',
    'nl' => 'nl',
    'pl' => 'pl-PL',
    'pt-BR' => 'pt-BR',
    'ru' => 'ru',
    'sl' => 'sl',
    'sv' => 'sv',
    'tr' => 'tr',
    'uk' => 'uk',
    'vi' => 'vi',
    'zh-Hans' => 'zh-Hans',
    'zh-Hant' => 'zh-Hant'
  }

  # to => from, since to is unique but from may not be
  manually_copied_languages = {
    'es-MX' => 'es'
  }

  # make sure the previous map has everything. adding a new language should error.
  ios_languages = Set.new(
    Dir.children(resources_dir_full)
    .select { |file| File.extname(file) == '.lproj' }
    .map { |file| File.basename(file, File.extname(file)) }
    .reject { |lang| lang == 'Base' }
  )

  mapped_ios_languages = Set.new(lang_frontend_to_ios.values + manually_copied_languages.keys)

  unless ios_languages == mapped_ios_languages
    missing = ios_languages - mapped_ios_languages
    UI.user_error!("missing language in map. missing: #{missing.to_a.join(', ')}")
  end

  language_mapping = lang_frontend_to_ios
                     .map { |key, value| { 'original_language_iso' => key, custom_language_iso: value } }

  sh(
    'lokalise2',
    '--token', token,
    '--project-id', frontend_project_id,
    'file', 'download',
    '--format', 'strings',
    '--filter-langs', lang_frontend_to_ios.keys.join(','),
    '--language-mapping', language_mapping.to_json.to_s,
    '--original-filenames=false',
    '--bundle-structure', '%LANG_ISO%.lproj/Frontend.%FORMAT%',
    '--export-empty-as', 'base',
    '--export-sort', 'a_z',
    '--replace-breaks=false',
    '--include-comments=false',
    '--include-description=false',
    '--unzip-to', resources_dir_full,
    log: false
  )

  manually_copied_languages.each do |to, from|
    FileUtils.cp(
      "#{resources_dir_full}/#{from}.lproj/Frontend.strings",
      "#{resources_dir_full}/#{to}.lproj/Frontend.strings"
    )
  end

  sh(
    'lokalise2',
    '--token', token,
    '--project-id', core_project_id,
    'file', 'download',
    '--format', 'strings',
    '--filter-langs', lang_frontend_to_ios.keys.join(','),
    '--language-mapping', language_mapping.to_json.to_s,
    '--original-filenames=false',
    '--bundle-structure', '%LANG_ISO%.lproj/Core.%FORMAT%',
    '--export-empty-as', 'base',
    '--export-sort', 'a_z',
    '--replace-breaks=false',
    '--include-comments=false',
    '--include-description=false',
    '--unzip-to', resources_dir_full,
    log: false
  )

  manually_copied_languages.each do |to, from|
    FileUtils.cp(
      "#{resources_dir_full}/#{from}.lproj/Core.strings",
      "#{resources_dir_full}/#{to}.lproj/Core.strings"
    )
  end

  sh('cd ../ && ./Pods/SwiftGen/bin/swiftgen')
end

desc 'Upload localized strings to Lokalise'
lane :push_strings do
  source_directories = [
    '../Sources/App/Resources/en.lproj'
  ]

  token = ENV.fetch('LOKALISE_API_TOKEN') do
    prompt(
      text: 'API token',
      secure_text: true
    )
  end

  project_id = ENV.fetch('LOKALISE_PROJECT_ID') do
    prompt(
      text: 'Project ID',
      secure_text: true
    )
  end

  source_directories.each do |directory|
    puts "Enumerating #{directory}..."
    Dir.each_child(directory) do |file|
      next if ['Frontend.strings', 'Core.strings'].include?(file)

      puts "Uploading file #{file}"
      sh(
        'lokalise2',
        '--token', token,
        '--project-id', project_id,
        'file', 'upload',
        '--file', "#{directory}/#{file}",
        '--lang-iso', 'en',
        log: false
      )
    end
  end
end

desc 'Find unused localized strings'
lane :unused_strings do
  files = [
    '../Sources/App/Resources/en.lproj/Localizable.strings'
  ]
  files.each do |file|
    puts "Looking at #{file}"
    unused_strings = File.read(file)
                         # grab the keys only
                         .scan(/"([^"]+)" = [^;]+;/)
                         # replace _ in the keys with nothing, which is what swiftgen does
                         .map { |s| [s[0], s[0].gsub('_', '')] }
                         # ignore any keys at the root level (aka like ok_label)
                         .select { |full, _key| full.include?('.') }
                         # find any strings that don't have matches in code
                         .select { |_full, key| system("git grep --ignore-case --quiet #{key} -- ../*.swift") == false }
    unused_strings.each_key { |full| puts full }
    puts "- Found #{unused_strings.count} unused strings"
  end
end

desc 'Upload App Store Connect metadata to Lokalise'
lane :update_lokalise_metadata do
  lokalise_metadata(action: 'update_lokalise', override_translation: true)
end

desc 'Download App Store Connect metadata from Lokalise and upload to App Store Connect Connect'
lane :update_asc_metadata do
  lokalise_metadata(action: 'update_itunes')
end

private_lane :set_version_info do |options|
  File.write(
    '../Configuration/Version.xcconfig',
    "MARKETING_VERSION=#{options[:version]}\nCURRENT_PROJECT_VERSION=#{options[:build]}\n"
  )
end

private_lane :get_xcconfig_marketing_version do
  sh('cd .. ; . Configuration/Version.xcconfig ; echo $MARKETING_VERSION').strip!
end

private_lane :get_xcconfig_build_number do
  sh('cd .. ; . Configuration/Version.xcconfig; echo $CURRENT_PROJECT_VERSION').strip!
end

desc 'Set version number'
lane :set_version do |options|
  version = options[:version]

  unless version
    if is_ci
      UI.error 'no version provided'
    else
      version = prompt(text: 'Version number: ')
    end
  end

  set_version_info(
    version: version,
    build: get_xcconfig_build_number
  )
end

desc 'Setup Continous Integration'
lane :setup_ha_ci do
  raise 'No github run number specified' unless ENV['GITHUB_RUN_NUMBER']

  set_version_info(
    version: get_xcconfig_marketing_version,
    build: get_xcconfig_build_number + '.' + ENV.fetch('GITHUB_RUN_NUMBER', nil) # rubocop:disable Style/StringConcatenation
  )

  # we only expose these keys in ci when appropriate
  ENV['FASTLANE_DONT_STORE_PASSWORD'] = '1'
  ENV['FASTLANE_PASSWORD'] = ENV.fetch('HOMEASSISTANT_APP_STORE_CONNECT_PASSWORD', nil)
  ENV['FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD'] = ENV.fetch('HOMEASSISTANT_APP_STORE_CONNECT_PASSWORD', nil)

  keychain_name = 'HomeAssistant-Keychain'
  keychain_password = SecureRandom.hex

  begin
    delete_keychain(name: keychain_name)
  rescue StandardError
    # we don't care if it didn't exist yet
  end

  create_keychain(
    name: keychain_name,
    password: keychain_password,
    timeout: 3600,
    unlock: true,
    add_to_search_list: true
  )

  KeyAndValue = Struct.new(:key, :value) # rubocop:disable Lint/ConstantDefinitionInBlock

  [
    KeyAndValue.new(ENV.fetch('P12_KEY_IOS_APP_STORE', nil), ENV.fetch('P12_VALUE_IOS_APP_STORE', nil)),
    KeyAndValue.new(ENV.fetch('P12_KEY_MAC_APP_STORE', nil), ENV.fetch('P12_VALUE_MAC_APP_STORE', nil)),
    KeyAndValue.new(ENV.fetch('P12_KEY_MAC_DEVELOPER_ID', nil), ENV.fetch('P12_VALUE_MAC_DEVELOPER_ID', nil))
  ].each do |info|
    tmp_file = '/tmp/import.p12'
    File.write(tmp_file, Base64.decode64(info.value))

    import_certificate(
      certificate_path: tmp_file,
      certificate_password: info.key,
      keychain_name: keychain_name,
      keychain_password: keychain_password
    )

    FileUtils.rm(tmp_file)
  end

  import_provisioning_profiles
end

desc 'Run tests'
lane :test do
  run_tests(
    workspace: 'HomeAssistant.xcworkspace',
    scheme: 'Tests-Unit',
    result_bundle: true,
    skip_package_dependencies_resolution: true,
    destination: 'platform=iOS Simulator,name=iPhone 16,OS=26.0'
  )
end

private_lane :provisioning_profile_specifiers do |options|
  # gym doesn't parse the provisioning profile specifier, so we need to do it ourselves
  all_targets_result = sh([
    'xcodebuild',
    '2>/dev/null', # this command started outputting warnings in Xcode 12.5
    '-json',
    '-showBuildSettings',
    '-sdk', options[:sdk],
    '-project', '../HomeAssistant.xcodeproj',
    '-scheme', 'App-Release',
    '-list'
  ] + [], log: false)
  all_targets = JSON.parse(all_targets_result)['project']['targets'].map { |t| "-target #{t}" }

  settings_result = sh([
    'xcodebuild',
    '2>/dev/null', # this command started outputting warnings in Xcode 12.5
    '-json',
    '-showBuildSettings',
    '-sdk', options[:sdk],
    '-project', '../HomeAssistant.xcodeproj'
  ] + all_targets, log: false)

  settings = JSON.parse(settings_result)

  specifiers = {}
  settings.each do |target_info|
    specifier = target_info['buildSettings']['PROVISIONING_PROFILE_SPECIFIER']
    bundle_identifier = target_info['buildSettings']['PRODUCT_BUNDLE_IDENTIFIER']
    next unless specifier && bundle_identifier

    specifiers[bundle_identifier] = specifier
  end

  specifiers
end

private_lane :upload_binary_to_apple do |options|
  attempts = 0

  begin
    sh(
      'xcrun', 'altool', '--upload-app', '--type', options[:type],
      '--file', options[:path],
      '--username', ENV.fetch('DELIVER_USERNAME', nil),
      '--password', '@env:FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD'
    )
  rescue StandardError => e
    puts "Failed with #{e}; retrying upload..."

    # retry a few times, then give up
    attempts += 1

    retry if attempts <= 3
    raise e
  end
end

platform :ios do
  private_lane :create_archive do
    setup_ha_ci if is_ci
    specifiers = provisioning_profile_specifiers(sdk: 'iphoneos')
    ipa_path = build_ios_app(
      export_method: 'app-store',
      skip_package_dependencies_resolution: true,
      skip_profile_detection: true,
      disable_xcpretty: true,
      output_directory: './build/ios',
      export_options: {
        signingStyle: 'manual',
        provisioningProfiles: specifiers
      }
    )
    if ENV['EMERGE_API_TOKEN']
      emerge(
        repo_name: ENV.fetch('EMERGE_REPO_NAME', nil),
        pr_number: ENV.fetch('EMERGE_PR_NUMBER', nil),
        sha: ENV.fetch('EMERGE_SHA', nil),
        base_sha: ENV.fetch('EMERGE_BASE_SHA', nil)
      )
    end
    ipa_path
  end

  lane :build do
    ipa_path = create_archive
    upload_binary_to_apple(
      type: 'ios',
      path: ipa_path
    )
  end

  lane :size do
    create_archive
  end
end

platform :mac do
  lane :build do
    setup_ha_ci if is_ci

    specifiers = provisioning_profile_specifiers(sdk: 'macosx')
    developer_id_app_path = build_mac_app(
      export_method: 'developer-id',
      skip_package_dependencies_resolution: true,
      skip_profile_detection: true,
      skip_package_pkg: true,
      disable_xcpretty: true,
      output_directory: './build/macos',
      export_options: {
        signingStyle: 'manual',
        provisioningProfiles: specifiers
      }
    )
    app_store_pkg_path = build_mac_app(
      archive_path: lane_context[SharedValues::XCODEBUILD_ARCHIVE],
      export_method: 'app-store',
      skip_package_dependencies_resolution: true,
      skip_profile_detection: true,
      skip_build_archive: true,
      skip_package_pkg: false,
      output_directory: './build/macos',
      export_options: {
        signingStyle: 'manual',
        provisioningProfiles: specifiers.transform_values { |v| v.sub('Mac Dev ID', 'Mac App Store') }
      }
    )

    notarize_attempts = 0

    begin
      notarize(
        package: developer_id_app_path,
        # not the _app_ bundle id, just an id that notarize uses for referencing
        bundle_id: 'io.home-assistant.fastlane.developer-id',
        verbose: true
      )
    rescue StandardError => e
      puts "Failed with #{e}; retrying notarize in a few seconds..."

      sleep 5

      # retry a few times, then give up
      notarize_attempts += 1

      retry if notarize_attempts <= 3
      raise e
    end

    sh(
      'ditto',
      '-c',
      '-k',
      '--sequesterRsrc',
      '--keepParent',
      File.expand_path(developer_id_app_path),
      '../build/macos/home-assistant-mac.zip'
    )
    upload_binary_to_apple(
      type: 'osx',
      path: app_store_pkg_path
    )
  end
end
